/**
 * Authors:
 * Vladimír Matěna
 * Jan Skalický
 */
package inireader;

import java.io.BufferedWriter;
import java.io.IOException;
import java.util.regex.Pattern;

/**
 * Class representing single element.
 */
public class Element
{
	/**
	 * Abstract class representing number and its correctness.
	 */
	private static abstract class NumberValue
	{
		protected boolean isCorrectValue;

		/**
		 * Decode number representation of value in string.
		 * @param value value to be decoded
		 * @return representation of number in String value
		 */
		protected static int decodeRepresentation(String value)
		{
			String firstTwoLetters;

			if (value.charAt(0) == '-' || value.charAt(0) == '+')
			{
				firstTwoLetters = value.substring(1, Math.min(3, value.length()));
			}
			else
			{
				firstTwoLetters = value.substring(0, Math.min(2, value.length()));
			}

			if (firstTwoLetters.equals("0x"))
			{
				return 16;
			}
			else if (firstTwoLetters.equals("0b"))
			{
				return 2;
			}
			else if (firstTwoLetters.charAt(0) == '0')
			{
				return 8;
			}
			else
			{
				return 10; // default
			}
		}
	}

	/**
	 * Class representing long and its correctness.
	 */
	private static final class LongValue extends NumberValue
	{
		private long longValue;

		/**
		 * Creates LongValue.
		 * @param value long value
		 * @param isCorrectValue tells, if value is correct
		 */
		public LongValue(long value, boolean isCorrectValue)
		{
			this.longValue = value;
			this.isCorrectValue = isCorrectValue;
		}

		/**
		 * Decodes LongValue from String.
		 * @param value String to decode
		 * @return LongValue decoded from value
		 */
		static LongValue decodeLongNumber(String value)
		{
			int representation = decodeRepresentation(value);
			int sign = 1;

			String decodingValue = value;

			if (decodingValue.charAt(0) == '+')
			{
				decodingValue = decodingValue.substring(1);
			}
			else if (decodingValue.charAt(0) == '-')
			{
				sign = -1;
				decodingValue = decodingValue.substring(1);
			}

			// 0b12345, 0x12345
			if (representation == 2 || representation == 16)
			{
				decodingValue = decodingValue.substring(2);
			}
			// 012345
			else if (representation == 8)
			{
				decodingValue = decodingValue.substring(1);
			}

			LongValue number = new LongValue(0, true);

			int expPower = 0;
			for (int i = decodingValue.length() - 1; i >= 0; i--)
			{
				int digit = Character.digit(decodingValue.charAt(i), representation);

				if (digit == -1)
				{
					number.isCorrectValue = false;
					break;
				}
				else
				{
					number.longValue += digit * Math.pow(representation, expPower);
					expPower++;
				}
			}

			number.longValue *= sign;

			return number;
		}
	}

	/**
	 * Class representing double and its correctness.
	 */
	private static final class DoubleValue extends NumberValue
	{
		public double doubleValue;

		/**
		 * Creates DoubleValue.
		 * @param value double value
		 * @param isCorrectValue tells, if value is correct
		 */
		public DoubleValue(double value, boolean isCorrectValue)
		{
			this.doubleValue = value;
			this.isCorrectValue = isCorrectValue;
		}

		/**
		 * Decodes DoubleValue from String.
		 * @param value String to decode
		 * @return DoubleValue decoded from value
		 */
		static DoubleValue decodeDoubleNumber(String value)
		{
			int representation = 10;
			String decodingValue = value;

			// sign
			int sign = 1;
			if (decodingValue.charAt(0) == '+')
			{
				decodingValue = decodingValue.substring(1);
			}
			else if (decodingValue.charAt(0) == '-')
			{
				sign = -1;
				decodingValue = decodingValue.substring(1);
			}

			DoubleValue number = new DoubleValue(0, true);
			boolean dotOccured = false;

			// exponent
			LongValue exponent = new LongValue(0, true);
			int indexOfE = decodingValue.lastIndexOf('e');
			if (indexOfE == -1)
			{
				indexOfE = decodingValue.lastIndexOf('E');
			}

			if (indexOfE != -1 && decodingValue.length() > indexOfE + 1)
			{
				exponent = LongValue.decodeLongNumber(decodingValue.substring(indexOfE + 1));

				number.isCorrectValue = exponent.isCorrectValue;
				decodingValue = decodingValue.substring(0, indexOfE);
			}

			// mantisa
			int expPower = 0;
			for (int i = decodingValue.length() - 1; i >= 0; i--)
			{
				int digit = Character.digit(decodingValue.charAt(i), representation);

				if (decodingValue.charAt(i) == '.' && !dotOccured)
				{
					while (number.doubleValue > 1)
					{
						number.doubleValue /= 10; // it is already in decadic representation
						expPower = 0;
					}

					dotOccured = true;
				}
				else if (digit == -1)
				{
					number.isCorrectValue = false;
					break;
				}
				else
				{
					number.doubleValue += digit * Math.pow(representation, expPower);
					expPower++;
				}
			}

			number.doubleValue *= sign * Math.pow(10, exponent.longValue);

			return number;
		}
	}
	
	private String rawValue, value, normalizedValue;
	private Property property;
	private Comment comment;
	private boolean isReferenced;

	/**
	 * Creates element from its content and property it belongs to.
	 * @param value element content as string
	 * @param property property new element should belong to
	 */
	public Element(String value, Property property)
	{
		this.rawValue = value;
		this.value = trimSpaces(value);
		this.normalizedValue = Configurator.cleanFromBackSlashes(this.value);
		this.property = property;
	}

	/**
	 * Trims spaces from start and end of string.
	 * Lets space after backslash if there is at the end of string.
	 * @param toTrim string to trim
	 * @return trimmed string
	 */
	private String trimSpaces(String toTrim)
	{
		String trimmedString = toTrim.trim();

		if (trimmedString.endsWith("\\"))
		{
			trimmedString += ' ';
		}

		return trimmedString;
	}

	/**
	 * Validates element (in any case).
	 */
	void validateElement()
	{
		this.normalizedValue = normalizeValue(
				Configurator.cleanFromBackSlashes(replaceReferences(value)),
				property.getType());
	}

	/**
	 * Normalize value according to ElementType for quicker evaluating.
	 * @param value to normalize
	 * @param type type for normalizing
	 * @return normalized value
	 */
	private String normalizeValue(String value, ElementType type)
	{
		String normValue = "";

		if (type == ElementType.Boolean)
		{
			if (Pattern.matches("1|t|y|on|yes|enabled", value))
			{
				normValue = "true";
			}
			else if (Pattern.matches("0|f|n|off|no|disabled", value))
			{
				normValue = "false";
			}
			else
			{
				throw new ElementTypeCastException(
						value + " cannot be converted:",
						ElementType.String,
						ElementType.Boolean);
			}
		}
		else if (type == ElementType.Int)
		{
			LongValue normLongValue = LongValue.decodeLongNumber(value);

			if (normLongValue.isCorrectValue
					&& normLongValue.longValue <= Integer.MAX_VALUE
					&& normLongValue.longValue >= Integer.MIN_VALUE)
			{
				normValue = Long.toString(normLongValue.longValue);
			}
			else
			{
				throw new ElementTypeCastException(
						value + " cannot be converted:",
						ElementType.String,
						ElementType.Int);
			}
		}
		else if (type == ElementType.UInt)
		{
			LongValue normLongValue = LongValue.decodeLongNumber(value);

			if (normLongValue.isCorrectValue
					&& normLongValue.longValue <= (long)(Integer.MAX_VALUE) * 2 - 1
					&& normLongValue.longValue >= 0)
			{
				normValue = Long.toString(normLongValue.longValue);
			}
			else
			{
				throw new ElementTypeCastException(
						value + " cannot be converted:",
						ElementType.String,
						ElementType.UInt);
			}
		}
		else if (type == ElementType.Float)
		{ // 64 bit representation -> Double
			DoubleValue normDoubleValue = DoubleValue.decodeDoubleNumber(value);

			if (normDoubleValue.isCorrectValue)
			{
				normValue = Double.toString(normDoubleValue.doubleValue);
			}
			else
			{
				throw new ElementTypeCastException(
						value + " cannot be converted:",
						ElementType.String, ElementType.Float);
			}
		}
		else if (type == ElementType.Enum)
		{
			normValue = value;
		}
		else if (type == ElementType.String)
		{
			boolean commentedOut = false;
			String forbiddenChars = ";:,";

			for (int i = 0; i < value.length(); i++)
			{
				char currCheckedChar = value.charAt(i);
				if (currCheckedChar == '\\' && !commentedOut)
				{
					commentedOut = true;
				}
				else if (!commentedOut && forbiddenChars.contains(Character.toString(currCheckedChar)))
				{
					throw new ElementTypeCastException(
							value + " contains forbidden character and therefore cannot be converted:",
							ElementType.String, ElementType.String);
				}
				else
				{
					commentedOut = false;
				}
			}

			normValue = Configurator.cleanFromBackSlashes(value);
		}

		if (this.property.restriction != null)
		{
			this.property.restriction.validateRestrictedValue(normValue);
		}
		return normValue;
	}

	/**
	 * Gets current value of element as string ignoring it's type.
	 * @return element's value as String
	 */
	public String getCurrentStringValue()
	{
		return normalizedValue;
	}

	/**
	 * Gets value of element as it was stored in file.
	 * @return raw value as String
	 */
	public String getUnnormalizedValue()
	{
		return rawValue;
	}

	/**
	 * Set value of element.
	 * Beginning end ending white spaces will be trimmed in value, but saved into ini file.
	 * If you want to have white spaces at the start or end of value, you must comment it with \.
	 * @param value value to set
	 */
	public void setUnnormalizedValue(String value)
	{
		this.rawValue = value;
		this.value = trimSpaces(value);

		validateElement();
		this.property.section.configurator.validateAllElements();
	}

	/**
	 * Goes through string and replace references to elements
	 * in current configurator.
	 * @param value
	 * @return
	 */
	private String replaceReferences(String value)
	{
		StringBuilder replacingBuffer = new StringBuilder();
		boolean currentIsCommented = false;

		for (int i = 0; i < value.length(); i++)
		{
			if (currentIsCommented)
			{
				replacingBuffer.append(value.charAt(i));
				currentIsCommented = false;
			}
			else if (value.charAt(i) == '\\')
			{
				replacingBuffer.append(value.charAt(i));
				currentIsCommented = true;
			}
			else if (value.charAt(i) == '$' && value.charAt(i + 1) == '{')
			{
				String referencePath = readUntil(value.substring(i + 2), '}');

				if (referencePath != null && referencePath.matches("([^#]+#){1,2}[^#]+"))
				{
					replacingBuffer.append(readReference(referencePath));
					i += referencePath.length() + 2; // because of {}
				}
				else
				{
					replacingBuffer.append(value.charAt(i));
				}
			}
			else
			{
				replacingBuffer.append(value.charAt(i));
			}
		}

		return replacingBuffer.toString();
	}

	/**
	 * Reads string until it gets limiter char.
	 * @param stringToRead string to be read
	 * @param limiter char limiter
	 * @return If there is not limiter, returns null,
	 * otherwise substring to limiter
	 */
	private String readUntil(String stringToRead, char limiter)
	{
		for (int i = 0; i < stringToRead.length(); i++)
		{
			if (stringToRead.charAt(i) == limiter)
			{
				return stringToRead.substring(0, i);
			}
		}

		return null;
	}

	/**
	 * Read one reference.
	 * @param referencePath Path to referenced value
	 * section#property
	 * section#property#indexOfElement
	 * @return referenced value
	 */
	private String readReference(String referencePath)
	{
		String referencedValue;

		int delimIndex = referencePath.indexOf('#');
		int delim2Index = referencePath.indexOf('#', delimIndex + 1);

		String refSectionString = referencePath.substring(0, delimIndex);
		String refPropertyString;

		LongValue elementIndex;
		if (delim2Index != -1)
		{
			elementIndex = LongValue.decodeLongNumber(referencePath.substring(delim2Index + 1));
			refPropertyString = referencePath.substring(delimIndex + 1, delim2Index);
		}
		else
		{
			elementIndex = new LongValue(0, true);
			refPropertyString = referencePath.substring(delimIndex + 1, referencePath.length());
		}

		Section refSection = this.property.section.configurator.getSection(refSectionString);

		if (refSection != null)
		{
			Property refProperty = refSection.getProperty(refPropertyString);

			if (refProperty != null)
			{
				referencedValue = readPropertyElementValueSafely(refProperty, elementIndex);
			}
			else
			{
				throw new ValidatingException(String.format(
						"Referenced property %s in section %s from property %s was not found!",
						refPropertyString, refSectionString, this.property.getName().getId()));
			}
		}
		else
		{
			throw new ValidatingException(String.format(
					"Referenced section %s from property %s was not found!",
					refSectionString, this.property.getName().getId()));
		}

		return referencedValue;
	}

	/**
	 * Reads element value from property, checks reference loops.
	 * @param property property to be read
	 * @param elementIndex index of element
	 * @return value of element on elementIndex
	 */
	private String readPropertyElementValueSafely(Property property, LongValue elementIndex)
	{
		if (this.isReferenced)
		{
			throw new ValidatingException(String.format("Reference infinite loop in property %s!", property.getName().getId()));
		}

		this.isReferenced = true;

		String referencedValue;
		if (elementIndex.isCorrectValue
				&& elementIndex.longValue >= 0
				&& elementIndex.longValue < property.getElements().size())
		{
			referencedValue = property.getElementOnIndex((int)elementIndex.longValue).getCurrentStringValue();
		}
		else
		{
			throw new ValidatingException(String.format(
					"Referenced property %s in section %s from property %s has not element with index %d!",
					property.getName().getId(),
					property.section.getHeader().getId(),
					this.property.getName().getId(),
					elementIndex.longValue));
		}

		this.isReferenced = false;

		return referencedValue;
	}

	/**
	 * Gets content of boolean element.
	 * @return content as boolean
	 */
	public boolean toBool()
	{
		if (property.getType() == ElementType.Boolean)
		{
			return getCurrentStringValue().equals("true") ? true : false;
		}

		throw new ElementTypeCastException(property.getType(), ElementType.Boolean);
	}

	/**
	 * Gets content of float element.
	 * @return content as double (64 bit representation -> double)
	 */
	public double toFloat()
	{
		if (property.getType() == ElementType.Float)
		{
			return Double.valueOf(getCurrentStringValue()).doubleValue();
		}

		throw new ElementTypeCastException(property.getType(), ElementType.Float);
	}

	/**
	 * Gets content of integer element.
	 * @return content as integer
	 */
	public int toInt()
	{
		if (property.getType() == ElementType.Int)
		{
			return Integer.valueOf(getCurrentStringValue()).intValue();
		}

		throw new ElementTypeCastException(property.getType(), ElementType.Int);
	}

	/**
	 * Gets content of string element.
	 * @return content as string
	 */
	@Override
	public String toString()
	{
		if (property.getType() == ElementType.String)
		{
			return getCurrentStringValue();
		}

		throw new ElementTypeCastException(property.getType(), ElementType.String);
	}

	/**
	 * Gets content of Enum element.
	 * @return content as string
	 */
	public String toEnum()
	{
		if (property.getType() == ElementType.Enum)
		{
			return getCurrentStringValue();
		}

		throw new ElementTypeCastException(property.getType(), ElementType.Enum);
	}

	/**
	 * Gets content of long element.
	 * @return content as long
	 */
	public long toUInt()
	{
		if (property.getType() == ElementType.UInt)
		{
			return Long.valueOf(getCurrentStringValue()).longValue();
		}

		throw new ElementTypeCastException(property.getType(), ElementType.UInt);
	}

	/**
	 * Formats element as human readable string for debugging purposes.
	 * @return element as human readable string
	 */
	String getElementString()
	{
		StringBuilder element = new StringBuilder();
		element.append("\t\tElement:\"");
		element.append(value);
		element.append("\"");

		if (comment != null)
		{
			element.append("\tComment:\"");
			element.append(comment.getContent());
			element.append("\"");
		}

		element.append(Configurator.NEWLINE);

		return element.toString();
	}

	/**
	 * Sets new comment.
	 * @param comment new comment
	 */
	public void setComment(Comment comment)
	{
		this.comment = comment;
	}

	/**
	 * Writes element content to stream.
	 * @param output the stream
	 * @throws IOException
	 */
	void save(BufferedWriter output) throws IOException
	{
		output.write(rawValue);

		if (comment != null)
		{
			output.write(comment.getContent());
		}
	}
}//end Element

